package msu.ml.data;

import msu.ml.core.*;
import java.util.*;
import java.io.*;

/**
 * The DataCache offers a way to make loading files more efficient.
 * Often loading files can be a very long and/or memory
 * consuming process. This is especially true if the file is stored
 * in ascii or if there is a lot of processing that must be done to
 * the data. The DataCache achieves better efficiency by loading the file
 * once and performing all data preprocessing actions on the data, afterwards
 * the data is written back to disc in a format that can be efficiently 
 * reloaded. In the future the DataCache will also offer in-memory caching.
 *
 * @author Reginald M Mead
 * @version 1.0
 */
public class DataCache 
{

   protected boolean verbose;
   protected boolean cacheToDisk;
   protected Hashtable<String, NxInstances> memCache;
   protected HashSet<String> fileCache;
   protected ArrayList<IDataLoader> m_IDataLoader;
   protected ArrayList<IDataPreProcessor> m_IDataPreProcessor;
   protected ArrayList<IDataPreProcessor> postCacheDataProcessors;
   protected ArrayList<IDataCacheEventListener> listeners;


   /**
    * Creates a new DataCache
    */
   public DataCache()
   {
      m_IDataLoader = new ArrayList<IDataLoader>();
      m_IDataPreProcessor = new ArrayList<IDataPreProcessor>();
      postCacheDataProcessors = new ArrayList<IDataPreProcessor>();
      listeners = new ArrayList<IDataCacheEventListener>();
      memCache = new Hashtable<String, NxInstances>();  
      fileCache = new HashSet<String>();
      cacheToDisk = true;

      this.verbose = ApplicationSettings.getInstance().getConfig(
            "verbose").equals("true");
   }

   /**
    * Add a data loader to the the set of loaders
    * that will be used when files are requested.
    *
    * @param loader IDataLoader to add
    */
   public void addDataLoader(IDataLoader loader)
   {
      m_IDataLoader.add(loader);

   }
   /**
    * Add a data preprocessor to use after data is initially loaded.
    * This is analogous to the concept of Filters in Weka.
    *
    * @param processor IDataPreProcessor to add
    */
   public void addDataPreProcessor(IDataPreProcessor processor)
   {
      m_IDataPreProcessor.add(processor);
   }

   public void addDataPostCacheProcessor(IDataPreProcessor processor)
   {
      postCacheDataProcessors.add(processor);
   }

   public void AddDataCacheListener(IDataCacheEventListener l)
   {
      this.listeners.add(l);
   }

   public void RemoveDataCacheListener(IDataCacheEventListener l)
   {
      this.listeners.remove(l);
   }

   public ArrayList<IDataLoader> getDataLoaders()
   {
      return m_IDataLoader;
   }

   public ArrayList<IDataPreProcessor> getDataPreProcessors()
   {
      return m_IDataPreProcessor;
   }

   protected void fireDataCacheEvent(String description)
   {
      DataCacheEvent evt = new DataCacheEvent(this, description);
      for(IDataCacheEventListener listener : listeners)
         listener.cacheEventReceived(evt);
   }

   protected NxInstances cache(String target)
   {
      if(verbose)
         System.out.println("Caching " + target + "...");

      fireDataCacheEvent("Caching " + target + "...");

      try
      {
         NxInstances data = load(target);
         ObjectOutputStream sout = new ObjectOutputStream(
               new FileOutputStream(target+".cache"));
         sout.writeObject(data);
         sout.close();

         fileCache.add(target);
         return data;
      }
      catch(Exception e) 
      {
         System.out.println(e.getMessage());
         System.out.println(e.getStackTrace());
      }

      return null;
   }

   protected NxInstances unCache(String target)
   {
      try
      {
         if(verbose)
            System.out.println("Uncaching " + target + "...");

         fireDataCacheEvent("Uncaching " + target + "...");

         ObjectInputStream sin = new ObjectInputStream(
               new FileInputStream(target+".cache"));
         NxInstances data = (NxInstances)sin.readObject();
         return data;
      }
      catch(Exception e) 
      {
         System.out.println(e.getMessage());
         System.out.println(e.getStackTrace());
      }

      return null;
   }

   /**
    * Precache the specified file
    *
    * @param target file to precache
    */
   public void preCache(String target)
   {
      if(!(fileCache.contains(target) || (new File(target+".cache")).exists()))
         cache(target);
   }

   /**
    * Returns true if the file is in a format that can be loaded by one
    * of the IDataLoaders contained in this DataCache object.
    *
    * @param target file to load
    * @return true if target can be loaded
    */
   public boolean canLoad(String target)
   {
      for(IDataLoader loader : m_IDataLoader)
      {
         if(loader.isSupportedData(target))
         {
            return true;
         }
      }
      return false;
   }

   /**
    * Retrieve the specified file. This is done by either loading
    * the raw file and caching or by uncaching an already cached 
    * file.
    *
    * @param target file to retrieve
    * @return loaded file
    */
   public NxInstances retrieveUnprocessed(String target)
   {
      NxInstances data = null;

      if(this.cacheToDisk)
      {
         if(fileCache.contains(target) || (new File(target+".cache")).exists())
            data = unCache(target);
         else
            data = cache(target); 
      }
      else
      {
         data = loadUnprocessed(target);
      }

      return data;
   }
   /**
    * Retrieve the specified file. This is done by either loading
    * the raw file and caching or by uncaching an already cached 
    * file.
    *
    * @param target file to retrieve
    * @return loaded file
    */
   public NxInstances retrieve(String target)
   {
      /*if(memCache.containsKey(target))
        return memCache.get(target);*/

      NxInstances data = retrieveUnprocessed(target);

      if(data != null)
      {
         for(IDataPreProcessor processor : postCacheDataProcessors)
            data = processor.process(data);
      }

      return data;
   }

   protected NxInstances loadUnprocessed(String target)
   {
      for(IDataLoader loader : m_IDataLoader)
         if(loader.isSupportedData(target))
            return loader.load(target); 
      return null;
   }

   protected NxInstances load(String target)
   {

      NxInstances result = loadUnprocessed(target); 
      if(result != null)
      {
         for(IDataPreProcessor processor : m_IDataPreProcessor)
            result = processor.process(result);

         if(verbose && result != null)
            System.out.println(target + " value = " + result.instance(0).value(result.instance(0).numAttributes() - 1));
      }
      return result;
   }

   /**
    * Returns whether files will be cached to disk.
    *
    * @return true if files will be cached to disk
    */
   public boolean getCacheToDisk()
   {
      return this.cacheToDisk;
   }

   /**
    * Set whether files will be cached to disk after they
    * are loaded.
    *
    * @param value true if files will be cached to disk
    */
   public void setCacheToDisk(boolean value)
   {
      this.cacheToDisk = value;
   }

}
